﻿#region

#if FRAMEWORK35
using System.Threading;
using Microsoft.CSharp;
using Microsoft.VisualBasic;
#else
using System.Threading.Tasks;
#endif
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using OMS.Ice.T4Generator.CodeBuilder;
using OMS.Ice.T4Generator.Syntax;

#endregion


namespace OMS.Ice.T4Generator
{
    /// <summary>
    ///   The Generator class. This class implements the IGenerator interface.
    /// </summary>
    public class Generator : IGenerator
    {
#if !FRAMEWORK35
        private readonly Dictionary<string, Task<Type>> _compilationTasks = new Dictionary<string, Task<Type>>();
#else
        private readonly Dictionary<string, Type> _generators = new Dictionary<string, Type>();
#endif //FRAMEWORK35

        /// <summary>
        ///   Constructor.
        /// </summary>
        public Generator()
        {
            Settings = new GeneratorSettings();
        }

        private IGeneratorSettings Settings { get; set; }


        #region IGenerator Members

        IGeneratorSettings IGenerator.Settings
        {
            get { return Settings; }
        }


        void IGenerator.GenerateToFile( string template, string output, params object[] parameters )
        {
            GenerateToFile( template, Encoding.UTF8, output, parameters );
        }


        void IGenerator.GenerateToFile( string template, Encoding encoding, string output, params object[] parameters )
        {
            GenerateToFile( template, encoding, output, parameters );
        }


        void IGenerator.Generate( string template, TextWriter output, params object[] parameters )
        {
            try
            {
                Generate( template, output, parameters );
            }
            catch( T4Exception )
            {
                throw;
            }
            catch( Exception e )
            {
                throw new T4Exception( string.Format( "Error while generating template '{0}'. See inner exception.", template ), e );
            }
        }

#if !FRAMEWORK35
        Task IGenerator.Compile(IEnumerable<string> templates)
        {
            var task = new Task(() => Parallel.ForEach(templates, template => GetGeneratorType(template)));
            task.Start();

            return task;
        }
#else
        void IGenerator.Compile(IEnumerable<string> templates)
        {
            ThreadPool.QueueUserWorkItem( (o) =>
                                          {
                                              foreach (var template in templates)
                                                  GetGeneratorType(template);
                                          });
        }
#endif //FRAMEWORK35

        #endregion

        private Type GetGeneratorType(string template)
        {
#if !FRAMEWORK35
            Task<Type> task;

            lock (_compilationTasks)
            {
                if( !_compilationTasks.TryGetValue( template, out task ) )
                {
                    task = new Task<Type>( () => Compile( template ) );

                    _compilationTasks.Add( template, task );

                    task.Start();
                }
            }

            if (!task.IsCompleted)
                task.Wait();

            return task.Result;
#else
            lock (this)
            {
                Type generatorType;

                // Try to get an already compiled generator.
                if (!_generators.TryGetValue(template, out generatorType))
                {
                    // Could not get a generator. Create a new.
                    generatorType = Compile(template);
                    _generators.Add(template, generatorType);
                }

                return generatorType;
            }
#endif //FRAMEWORK35
        }

        private void GenerateToFile( string template, Encoding encoding, string output, params object[] parameters )
        {
            var path = Path.GetDirectoryName( output );
            if( string.IsNullOrEmpty( path ) )
                throw new T4Exception( string.Format( "Missing output path." ) );

            try
            {
                if( !Directory.Exists( path ) )
                    Directory.CreateDirectory( path );

                using( var writer = new StreamWriter( output, false, encoding ) {AutoFlush = false} )
                {
                    Generate( template, writer, parameters );
                    writer.Close();
                }
            }
            catch( T4Exception )
            {
                throw;
            }
            catch( Exception e )
            {
                throw new T4Exception( string.Format( "Error while generating template '{0}'. See inner exception.", template ), e );
            }
        }


        private void Generate( string template, TextWriter output, params object[] parameters )
        {
            var generatorType = GetGeneratorType(template);

            // Get the constructor for the generator,
            var constructor = generatorType.GetConstructor((from p in parameters
                                                                           select p.GetType()).ToArray() );
#if !FRAMEWORK35
// ReSharper disable PossibleNullReferenceException
            dynamic generator = constructor.Invoke( parameters );
// ReSharper restore PossibleNullReferenceException

            generator.Generate( output );
#else
// ReSharper disable PossibleNullReferenceException
            var generator = constructor.Invoke( parameters );
// ReSharper restore PossibleNullReferenceException

            // Run the Generate() method.

            generatorType.InvokeMember( "Generate", BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod, null, generator,
                                                      new object[] {output} );
#endif
            //!FRAMEWORK35            

            // Flush the stream.
            output.Flush();
        }

        private Type Compile( string template )
        {
            // Parse the template
            var parser = new Parser(Settings);
            var parserResult = parser.Parse( template, null );

            // Get the language.
// ReSharper disable PossibleMultipleEnumeration
            var templateDirective = (from p in parserResult.Parts
                                     where p is TemplateDirective
                                     select (TemplateDirective)p).First();
// ReSharper restore PossibleMultipleEnumeration

            // Get a code builder.
            var codeBuilder = GetCodeBuilder( templateDirective.Language );

            // Generate the code for the generator.
// ReSharper disable PossibleMultipleEnumeration
            var code = codeBuilder.Generate( parserResult, Settings.EndOfLine );

            var codeFileName = (from p in parserResult.Parts
                                where p is TemplateDirective
                                select ((TemplateDirective)p).CodeFile).First();

            var codeFile = string.Empty;
            if( !string.IsNullOrEmpty( codeFileName ) )
            {
// ReSharper disable AssignNullToNotNullAttribute
                codeFileName = Path.Combine( Path.GetDirectoryName( template ), codeFileName );
// ReSharper restore AssignNullToNotNullAttribute
                codeFile = File.OpenText( codeFileName ).ReadToEnd();
            }

            var assemlyPaths = GetReferencedAssemlies( parserResult );
// ReSharper restore PossibleMultipleEnumeration

            // Create the generator assembly.
            var provider = GetCodeDomProvider( templateDirective.Language, templateDirective.CompilerVersion );
            var parameter = new CompilerParameters( assemlyPaths ) //, codeBuilder.ClassName + ".dll" )
                            {GenerateExecutable = false, GenerateInMemory = true}; //, IncludeDebugInformation = true};

            var results = provider.CompileAssemblyFromSource( parameter, new[] {code, codeFile} );
            if( results.Errors.HasErrors )
            {
                throw new T4CompilerException( "Error while building generator assembly.",
                                               (from e in results.Errors.Cast<CompilerError>()
                                                select e).ToList(),
                                               code, codeFile );
            }

            // Return the generator type.
            var assembly = results.CompiledAssembly;
            return assembly.GetType( codeBuilder.GeneratorType );
        }

        private string[] GetReferencedAssemlies( ParserResult parserResult )
        {
            // Get all referenced assemblies from the main template.
            var assemblies = (from p in parserResult.Parts
                              where p is AssemblyDirective
                              select ((AssemblyDirective)p).Name).ToList();

            assemblies = assemblies.Union( Settings.ReferenceAssemblies ).ToList();
            // Add all referenced assemblies from the included templates.
            assemblies = assemblies.Union( from i in parserResult.IncludedTemplates.Values
                                           from p in i
                                           where p is AssemblyDirective
                                           select ((AssemblyDirective)p).Name ).Distinct().ToList();

            // Include this assembly to the referenced assemblies list.
            assemblies.Insert( 0, Assembly.GetExecutingAssembly().Location );

            var referencePaths = GetReferencePaths();
            var assemlyPaths = assemblies.Select( assembly => FindAssembly( assembly, referencePaths ) ).ToList();

            // Add the System.dll as default. Thus it is not needed in the template file.
            assemlyPaths.Add( "System.dll" );

            return assemlyPaths.ToArray();
        }


        private static CodeBuilder.CodeBuilder GetCodeBuilder( string language )
        {
            CodeBuilder.CodeBuilder codeBuilder;
            switch( language )
            {
                case "C#":
                    codeBuilder = new CSCodeBuilder();
                    break;
                case "VB":
                    codeBuilder = new VBCodeBuilder();
                    break;
                default:
                    codeBuilder = new CSCodeBuilder();
                    break;
            }

            return codeBuilder;
        }

        private CodeDomProvider GetCodeDomProvider( string language, string compilerVersion )
        {
            if( !CodeDomProvider.IsDefinedLanguage( language ) )
                throw new T4Exception( string.Format( "Unknown language '{0}'.", language ) );

            // Make a copy of the settings.
            var options = new Dictionary<string, string>( Settings.Options );

            // Remove from options if added anywhere.
            if( options.ContainsKey( "CompilerVersion" ) )
                options.Remove( "CompilerVersion" );

            switch( Settings.CompilerVersion )
            {
                    // If CompilerVersion.Auto or CompilerVersion.Template and compilerVersion is empty
                    // let .NET choose the appropriate compiler version. No CompilerVersion property must be added to the options.
                case CompilerVersion.Template:
                    if( !string.IsNullOrEmpty( compilerVersion ) )
                        options.Add( "CompilerVersion", compilerVersion );
                    break;
                case CompilerVersion.V40:
                    options.Add( "CompilerVersion", "v4.0" );
                    break;
            }


#if !FRAMEWORK35
            return CodeDomProvider.CreateProvider( language, options );
#else
            
            switch( language )
            {
                case "C#":
                    return new CSharpCodeProvider( options );
                case "VB":
                    return new VBCodeProvider( options );
                default:
                    throw new T4Exception( string.Format( "Unknown language '{0}'.", language ) );
            }
#endif
        }


        private IEnumerable<string> GetReferencePaths()
        {
            var result = new List<string>();

            var domain = AppDomain.CurrentDomain;
            var baseDirectory = domain.BaseDirectory;
            result.Add(baseDirectory);


            if (!string.IsNullOrEmpty(domain.RelativeSearchPath))
            {
                result.AddRange((from d in domain.RelativeSearchPath.Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries)
                                 select Path.Combine(baseDirectory, d)));
            }

            foreach(var path in Settings.ReferencePaths )
            {
                if(path.EndsWith( "*" ))
                    result.AddRange( Directory.GetDirectories( path.Trim('*'), "*", SearchOption.AllDirectories ) );
                else
                    result.Add( path );
            }
            
            result.AddRange( Settings.ReferencePaths );
            
            return result.Distinct();
        }


        private string FindAssembly( string fileName, IEnumerable<string> referencePaths )
        {
            // No filename results in null path:
            if( string.IsNullOrEmpty( fileName ) )
                return null;

            // If filename contains commas, it is a strong name.
            if( fileName.Contains( ',' ) )
                return GetPathFromGAC( fileName );

            // If absolute path, just return it.
            if( Path.IsPathRooted( fileName ) )
                return fileName;

            // Search for the file in each reference directory.
            var path = referencePaths.Select( dir => Path.Combine( dir, fileName ) ).FirstOrDefault( File.Exists );

            if( string.IsNullOrEmpty( path ) )
                return fileName;

            return path;
        }

        private string GetPathFromGAC( string strongName )
        {
            try
            {
                var assembly = Assembly.Load( strongName );

                return assembly.Location;
            }
            catch( Exception e )
            {
                throw new T4Exception( string.Format( "Could not load strong named assembly: '{0}'", strongName ), e );
            }
        }


        #region Nested type: GeneratorSettings

        internal class GeneratorSettings : IGeneratorSettings
        {
            public GeneratorSettings()
            {
                ReferencePaths = new List<string>();
                ReferenceAssemblies = new List<string>();
                Options = new Dictionary<string, string>();
                TemplateParameters = new Dictionary<string, string>();
                EndOfLine = EndOfLine.CRLF;
            }


            #region IGeneratorSettings Members

            public IList<string> ReferencePaths { get; private set; }

            public IList<string> ReferenceAssemblies { get; private set; }

            public IDictionary<string, string> Options { get; private set; }

            public EndOfLine EndOfLine { get; set; }

            public CompilerVersion CompilerVersion { get; set; }

            public IDictionary<string, string> TemplateParameters { get; set; } 

            #endregion
        }

        #endregion
    }
}